/*
 * FlowComponentDefinition.java
 *
 * Created on September 8, 2006, 4:42 PM
 *
 * (C) 2006 R. Alexander Milowski
 */

package com.smallx.xproc.library;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.infoset.component.Component;
import org.infoset.component.ComponentBase;
import org.infoset.component.ComponentDefinition;
import org.infoset.xml.Document;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.Name;
import org.infoset.xml.NamespaceScope;
import org.infoset.xml.XMLException;

/**
 *
 * @author alex
 */
public class FlowGraphComponentDefinition implements ComponentDefinition {

   static Logger LOG = Logger.getLogger(FlowGraphComponentDefinition.class.getName());
   static class ComponentBinding {
      Name name;
      ComponentDefinition def;
      Map<Name,PortBinding> outputBinding;
      Map<Name,Object> staticInputs;
      Map<Name,String> staticParameters;
      NamespaceScope scope;
      ComponentBinding(Name name,ComponentDefinition def) {
         this.name = name;
         this.def = def;
         this.outputBinding = null;
         this.staticInputs = null;
         this.staticParameters = null;
         this.scope = null;
      }
      
      public void bindOutput(Name port,ComponentBinding target,Name input) {
         if (outputBinding==null) {
            outputBinding = new HashMap<Name,PortBinding>();
         }
         if (outputBinding.get(port)!=null) {
            throw new IllegalArgumentException("Component "+name+" already has a binding for "+port);
         }
         outputBinding.put(port,new PortBinding(target,port,input));
      }
      
      public void bindInput(Name port,Object source) {
         if (staticInputs==null) {
            staticInputs = new HashMap<Name,Object>();
         }
         staticInputs.put(port,source);
      }
      
      public void addParameter(Name name,String value) {
         if (staticParameters==null) {
            staticParameters = new HashMap<Name,String>();
         }
         staticParameters.put(name,value);
      }
      public void addParameters(Map<Name,String> parameters) {
         if (staticParameters==null) {
            staticParameters = new HashMap<Name,String>();
         }
         staticParameters.putAll(parameters);
      }
   }
   
   static class PortBinding {
      Name port;
      Name targetPort;
      ComponentBinding target;
      PortBinding(ComponentBinding target,Name targetPort,Name port) {
         this.target = target;
         this.targetPort = targetPort;
         this.port = port;
      }
   }
   
   static class PortInstance {
      Component component;
      Name name;
      PortInstance(Component c,Name name) {
         this.component = c;
         this.name = name;
      }
   }
   
   static class Notification {
      Name source;
      Name target;
      Notification(Name source,Name target) {
         this.source = source;
         this.target = target;
      }
   }
   
   protected Name pipelineType;
   protected Map<Name,PortBinding> inputs;
   protected Map<Name,PortBinding> outputs;
   protected Map<Name,ComponentBinding> components;
   protected List<Notification> notifications;
   protected Map<Name,Boolean> startEndComponent;
   
   /** Creates a new instance of FlowComponentDefinition */
   public FlowGraphComponentDefinition(Name pipelineType) {
      this.pipelineType = pipelineType;
      this.inputs = new HashMap<Name,PortBinding>();
      this.outputs = new HashMap<Name,PortBinding>();
      this.components = new HashMap<Name,ComponentBinding>();
      this.notifications = new ArrayList<Notification>();
      this.startEndComponent = new HashMap<Name,Boolean>();
   }
   
   public void addInputPort(Name name, Name destination, Name port) {
      ComponentBinding component = components.get(destination);
      if (component==null) {
         throw new IllegalArgumentException("Component "+destination+" does not exist.");
      }
      inputs.put(name,new PortBinding(component,port,name));
   }
   
   public void addOutputPort(Name name,Name source, Name port) {
      ComponentBinding component = components.get(source);
      if (component==null) {
         throw new IllegalArgumentException("Component "+source+" does not exist.");
      }
      outputs.put(name,new PortBinding(component,port,name));
   }
   
   public void addComponent(Name name,ComponentDefinition def, NamespaceScope scope) {
      if (components.get(name)!=null) {
         throw new IllegalArgumentException("Component binding for "+name+" already exists.");
      }
      ComponentBinding cbinding = new ComponentBinding(name,def);
      components.put(name,cbinding);
      cbinding.scope = scope;
      startEndComponent.put(name,Boolean.TRUE);
   }
   
   public void setComponentNested(Name name) {
      startEndComponent.put(name,Boolean.FALSE);
   }
   
   public void bindParameter(Name source,Name name,String value)
   {
      ComponentBinding component = components.get(source);
      if (component==null) {
         throw new IllegalArgumentException("Component "+source+" does not exist.");
      }
      component.addParameter(name,value);
   }
   
   public void bindParameters(Name source,Map<Name,String> parameters)
   {
      ComponentBinding component = components.get(source);
      if (component==null) {
         throw new IllegalArgumentException("Component "+source+" does not exist.");
      }
      component.addParameters(parameters);
   }
   
   public void bindInput(Name source,Name port,Object object)
   {
      ComponentBinding component = components.get(source);
      if (component==null) {
         throw new IllegalArgumentException("Component "+source+" does not exist.");
      }
      component.bindInput(port,object);
   }
   public void bindOutput(Name source,Name port,Name target,Name input)
   {
      ComponentBinding component = components.get(source);
      if (component==null) {
         throw new IllegalArgumentException("Component "+source+" does not exist.");
      }
      ComponentBinding targetComponent = components.get(target);
      if (targetComponent==null) {
         throw new IllegalArgumentException("Component "+target+" does not exist.");
      }
      component.bindOutput(port,targetComponent,input);
   }
   public void addNotification(Name source, Name target) {
      notifications.add(new Notification(source,target));
   }
   
   public Component newInstance()
      throws XMLException 
   {
      final Map<Name,ComponentInitializer> componentInstances = new HashMap<Name,ComponentInitializer>();
      
      // Create components and map static inputs
      for (ComponentBinding cbinding : components.values()) {
         Component c = cbinding.def.newInstance();
         ComponentInitializer initializer = new ComponentInitializer(cbinding.name,c);
         componentInstances.put(cbinding.name,initializer);
         // Map static inputs
         if (cbinding.staticInputs!=null) {
            for (Name port : cbinding.staticInputs.keySet()) {
               Object value = cbinding.staticInputs.get(port);
               initializer.addStaticInput(port,value);
            }
         }
         // map parameters
         if (cbinding.staticParameters!=null) {
            initializer.addParameters(cbinding.staticParameters);
         }
         initializer.setNamespaceScope(cbinding.scope);
      }
      
      // Map outputs to non-static inputs
      for (ComponentBinding cbinding : components.values()) {
         ComponentInitializer initializer = componentInstances.get(cbinding.name);
         if (cbinding.outputBinding!=null) {
            for (PortBinding binding : cbinding.outputBinding.values()) {
               ComponentInitializer target = componentInstances.get(binding.target.name);
               initializer.addDestinationAdapter(new DestinationAdapter(binding.targetPort,binding.port,target.getComponent()));
            }
         }
      }
      
      // Setup the input instances
      final Map<Name,PortInstance> pipeInputs = new HashMap<Name,PortInstance>();
      for (PortBinding binding : inputs.values()) {
         pipeInputs.put(binding.port,new PortInstance(componentInstances.get(binding.target.name).getComponent(),binding.targetPort));
      }
      
      // Setup the output instances
      final Map<Name,PortInstance> pipeOutputs = new HashMap<Name,PortInstance>();
      for (PortBinding binding : outputs.values()) {
         pipeOutputs.put(binding.port,new PortInstance(componentInstances.get(binding.target.name).getComponent(),binding.targetPort));
      }
      
      for (Notification n : notifications) {
         ComponentInitializer source = componentInstances.get(n.source);
         LOG.fine("Adding notification from "+n.source+" to "+n.target);
         ComponentInitializer target = componentInstances.get(n.target);
         if (source.getComponent() instanceof IterationNotifier) {
            IterationNotifier notifier = (IterationNotifier)source.getComponent();
            if (target.getComponent() instanceof IterationListener) {
               IterationListener listener = (IterationListener)target.getComponent();
               notifier.addIterationListener(listener);
            } else {
               final Component targetComponent = target.getComponent();
               notifier.addIterationListener(new IterationListener() {
                  final boolean isFineLog = LOG.isLoggable(Level.FINE);
                  public void notifyStart()
                     throws XMLException
                  {
                     if (isFineLog) {
                        LOG.fine("notify start on "+targetComponent.getClass().getName());
                     }
                     targetComponent.onStart();
                  }
                  public void notifyEnd()
                     throws XMLException
                  {
                     if (isFineLog) {
                        LOG.fine("notify end to "+targetComponent.getClass().getName());
                     }
                     targetComponent.onEnd();
                  }
               });
            }
         } else {
            throw new XMLException("Source "+n.source+" is not a iteration notifier but a notification to "+n.target+" was requested.");
         }
      }
      
      return new ComponentBase(pipelineType) {
         public void init(Component.Context context) 
            throws XMLException
         {
            for (ComponentInitializer initializer : componentInstances.values()) {
               initializer.setParentContext(context);
               initializer.init();
               initializer.bindStatics();
               initializer.bindOutputs();
            }
         }
         public void bindOutputPort(Name port,ItemDestination dest) 
            throws XMLException
         {
            PortInstance portSource = pipeOutputs.get(port);
            if (portSource==null) {
               throw new XMLException("There is no output port named "+port+" on the pipeline "+pipelineType);
            }
            portSource.component.bindOutputPort(portSource.name,dest);
         }

         public void onStart()
            throws XMLException
         {
            for (ComponentInitializer initializer : componentInstances.values()) {
               if (startEndComponent.get(initializer.getName())==Boolean.TRUE) {
                  initializer.getComponent().onStart();
               }
            }
         }

         public ItemDestination getInputPort(Name port) 
            throws XMLException
         {
            PortInstance portSource = pipeInputs.get(port);
            if (portSource==null) {
               throw new XMLException("There is no input port named "+port+" on the pipeline "+pipelineType);
            }
            return portSource.component.getInputPort(portSource.name);
         }

         public void onDocumentInput(Name port,Document doc) 
            throws XMLException
         {
            PortInstance portSource = pipeOutputs.get(port);
            if (portSource==null) {
               throw new XMLException("There is no input port named "+port+" on the pipeline "+pipelineType);
            }
            portSource.component.onDocumentInput(portSource.name,doc);
         }

         public void onEnd()
            throws XMLException
         {
            for (ComponentInitializer initializer : componentInstances.values()) {
               if (startEndComponent.get(initializer.getName())==Boolean.TRUE) {
                  initializer.getComponent().onEnd();
               }
            }
         }

      };
   }

   public Name getName() {
      return pipelineType;
   }

   public String getVersion() {
      return null;
   }

   public String getVendor() {
      return null;
   }

   public URI getVendorLocation() {
      return null;
   }

   public Collection<Name> getInputs() {
      return inputs.keySet();
   }

   public Collection<Name> getOutputs() {
      return outputs.keySet();
   }

}

